using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using NFreeMarker.Core;
using NFreeMarker.Debug.Impl;

namespace NFreeMarker.Template
{
    /**
     * <p>A core FreeMarker API that represents a compiled template.
     * Typically, you will use a {@link Configuration} object to instantiate a template.
     *
     * <PRE>
          Configuration cfg = new Configuration();
          ...
          Template myTemplate = cfg.getTemplate("myTemplate.html");
       </PRE>
     *
     * <P>However, you can also construct a template directly by passing in to
     * the appropriate constructor a java.io.Reader instance that is set to
     * read the raw template text. The compiled template is
     * stored in an an efficient data structure for later use.
     *
     * <p>To render the template, i.e. to merge it with a data model, and
     * thus produce "cooked" output, call the <tt>process</tt> method.
     *
     * <p>Any error messages from exceptions thrown during compilation will be
     * included in the output stream and thrown back to the calling code.
     * To change this behavior, you can install custom exception handlers using
     * {@link Configurable#setTemplateExceptionHandler(TemplateExceptionHandler)} on
     * a Configuration object (for all templates belonging to a configuration) or on
     * a Template object (for a single template).
     * 
     * <p>It's not legal to modify the values of FreeMarker settings: a) while the
     * template is executing; b) if the template object is already accessible from
     * multiple threads.
     * 
     * @version $Id: Template.java,v 1.216.2.3 2006/03/10 17:49:02 revusky Exp $
     */

    public class Template : Configurable
    {
        public const string DefaultNamespacePrefix = "D";
        public const string NoNsPrefix = "N";

        private readonly Dictionary<string, Macro> _macros = new Dictionary<string, Macro>();
        private readonly List<LibraryLoad> _imports = new List<LibraryLoad>();
        private TemplateElement _rootElement;
        private Encoding _encoding;
        private string _defaultNs;
        private readonly string _name;
        private readonly List<string> _lines = new List<string>();
        private readonly Dictionary<string, string> _prefixToNamespaceUriLookup = new Dictionary<string, string>();
        private readonly Dictionary<string, string> _namespaceUriToPrefixLookup = new Dictionary<string, string>();

        /**
         * A prime constructor to which all other constructors should
         * delegate directly or indirectly.
         */
        private Template(string name, Configuration cfg)
            : base(cfg ?? Configuration.GetDefaultConfiguration())
        {
            _name = name;
        }

            /**
             * Constructs a template from a character stream.
             *
             * @param name the path of the template file relative to the directory what you use to store
             *        the templates. See {@link #getName} for more details.
             * @param reader the character stream to read from. It will always be closed (Reader.close()).
             * @param cfg the Configuration object that this Template is associated with.
             *        If this is null, the "default" {@link Configuration} object is used,
             *        which is highly discouraged, because it can easily lead to
             *        erroneous, unpredictable behaviour.
             *        (See more {@link Configuration#getDefaultConfiguration() here...})
             * @param encoding This is the encoding that we are supposed to be using. If this is
             * non-null (It's not actually necessary because we are using a Reader) then it is
             * checked against the encoding specified in the FTL header -- assuming that is specified,
             * and if they don't match a WrongEncodingException is thrown.
             */
            public Template(string name, TextReader reader, Configuration cfg, Encoding encoding)
                : this(name, cfg)
            {
                _encoding = encoding;

                //if (!(reader is BufferedReader)) {
                //    reader = new BufferedReader(reader, 0x1000);
                //}
                var ltb = new LineTableBuilder(this, reader);
                try
                {
                    try
                    {
                        var parser = new FMParser(this, ltb,
                                GetConfiguration().GetStrictSyntaxMode(),
                                GetConfiguration().GetWhitespaceStripping(),
                                GetConfiguration().GetTagSyntax());
                        _rootElement = parser.Root();
                    }
                    catch (TokenMgrError exc)
                    {
                        throw new ParseException("Token manager error: " + exc, 0, 0);
                    }
                }
                catch (ParseException e)
                {
                    e.SetTemplateName(name);
                    throw;
                }
                finally
                {
                    ltb.Close();
                }
                //TODO:
                //DebuggerService.RegisterTemplate(this);
                //_namespaceUriToPrefixLookup = Collections.unmodifiableMap(_namespaceUriToPrefixLookup);
                //_prefixToNamespaceUriLookup = Collections.unmodifiableMap(_prefixToNamespaceUriLookup);
            }

            /**
             * This is equivalent to Template(name, reader, cfg, null)
             */
            public Template(string name, TextReader reader, Configuration cfg)
                : this(name, reader, cfg, null)
            {
            }

        //    /**
        //     * Constructs a template from a character stream.
        //     *
        //     * This is the same as the 3 parameter version when you pass null
        //     * as the cfg parameter.
        //     * 
        //     * @deprecated This constructor uses the "default" {@link Configuration}
        //     * instance, which can easily lead to erroneous, unpredictable behaviour.
        //     * See more {@link Configuration#getDefaultConfiguration() here...}.
        //     */
        //    public Template(string name, Reader reader) throws IOException {
        //        this(name, reader, null);
        //    }

        //    /**
        //     * This constructor is only used internally.
        //     */
        //    Template(string name, TemplateElement root, Configuration config) {
        //        this(name, config);
        //        this.rootElement = root;
        //        DebuggerService.registerTemplate(this);
        //    }

        /**
         * Returns a trivial template, one that is just a single block of
         * plain text, no dynamic content. (Used by the cache module to create
         * unparsed templates.)
         * @param name the path of the template file relative to the directory what you use to store
         *        the templates. See {@link #getName} for more details.
         * @param content the block of text that this template represents
         * @param config the configuration to which this template belongs
         */
        public static Template GetPlainTextTemplate(string name, string content, Configuration config)
        {
            var template = new Template(name, config);
            var block = new TextBlock(content);
            template._rootElement = block;
            // TODO: DebuggerService.RegisterTemplate(template);
            return template;
        }

            /**
             * Processes the template, using data from the map, and outputs
             * the resulting text to the supplied <tt>Writer</tt> The elements of the
             * map are converted to template models using the default object wrapper
             * returned by the {@link Configuration#getObjectWrapper() getObjectWrapper()}
             * method of the <tt>Configuration</tt>.
             * @param rootMap the root node of the data model.  If null, an
             * empty data model is used. Can be any object that the effective object
             * wrapper can turn into a <tt>TemplateHashModel</tt>. Basically, simple and
             * beans wrapper can turn <tt>java.util.Map</tt> objects into hashes
             * and the Jython wrapper can turn both a <tt>PyDictionary</tt> as well as
             * any object that implements <tt>__getitem__</tt> into a template hash.
             * Naturally, you can pass any object directly implementing
             * <tt>TemplateHashModel</tt> as well.
             * @param out a <tt>Writer</tt> to output the text to.
             * @throws TemplateException if an exception occurs during template processing
             * @throws IOException if an I/O exception occurs during writing to the writer.
             */
            public void Process(object rootMap, TextWriter output)
            {
                CreateProcessingEnvironment(rootMap, output, null).Process();
            }

            /**
             * Processes the template, using data from the root map object, and outputs
             * the resulting text to the supplied writer, using the supplied
             * object wrapper to convert map elements to template models.
             * @param rootMap the root node of the data model.  If null, an
             * empty data model is used. Can be any object that the effective object
             * wrapper can turn into a <tt>TemplateHashModel</tt> Basically, simple and
             * beans wrapper can turn <tt>java.util.Map</tt> objects into hashes
             * and the Jython wrapper can turn both a <tt>PyDictionary</tt> as well as any
             * object that implements <tt>__getitem__</tt> into a template hash.
             * Naturally, you can pass any object directly implementing
             * <tt>TemplateHashModel</tt> as well.
             * @param wrapper The object wrapper to use to wrap objects into
             * {@link TemplateModel} instances. If null, the default wrapper retrieved
             * by {@link Configurable#getObjectWrapper()} is used.
             * @param out the writer to output the text to.
             * @param rootNode The root node for recursive processing, this may be null.
             * 
             * @throws TemplateException if an exception occurs during template processing
             * @throws IOException if an I/O exception occurs during writing to the writer.
             */
            //public void Process(object rootMap, TextWriter output, IObjectWrapper wrapper, ITemplateNodeModel rootNode)
            //{
            //    Core.Environment env = CreateProcessingEnvironment(rootMap, output, wrapper);
            //    if (rootNode != null) {
            //        env.SetCurrentVisitorNode(rootNode);
            //    }
            //    env.Process();
            //}

            /**
             * Processes the template, using data from the root map object, and outputs
             * the resulting text to the supplied writer, using the supplied
             * object wrapper to convert map elements to template models.
             * @param rootMap the root node of the data model.  If null, an
             * empty data model is used. Can be any object that the effective object
             * wrapper can turn into a <tt>TemplateHashModel</tt> Basically, simple and
             * beans wrapper can turn <tt>java.util.Map</tt> objects into hashes
             * and the Jython wrapper can turn both a <tt>PyDictionary</tt> as well as any
             * object that implements <tt>__getitem__</tt> into a template hash.
             * Naturally, you can pass any object directly implementing
             * <tt>TemplateHashModel</tt> as well.
             * @param wrapper The object wrapper to use to wrap objects into
             * {@link TemplateModel} instances. If null, the default wrapper retrieved
             * by {@link Configurable#getObjectWrapper()} is used.
             * @param out the writer to output the text to.
             * 
             * @throws TemplateException if an exception occurs during template processing
             * @throws IOException if an I/O exception occurs during writing to the writer.
             */
            //public void Process(object rootMap, TextWriter output, IObjectWrapper wrapper)
            //{
            //    Process(rootMap, output, wrapper, null);
            //}

        /**
        * Creates a {@link freemarker.core.Environment Environment} object,
        * using this template, the data model provided as the root map object, and
        * the supplied object wrapper to convert map elements to template models.
        * You can then call Environment.process() on the returned environment
        * to set off the actual rendering.
        * Use this method if you want to do some special initialization on the environment
        * before template processing, or if you want to read the environment after template
        * processing.
        *
        * <p>Example:
        *
        * <p>This:
        * <pre>
        * Environment env = myTemplate.createProcessingEnvironment(root, out, null);
        * env.process();
        * </pre>
        * is equivalent with this:
        * <pre>
        * myTemplate.process(root, out);
        * </pre>
        * But with <tt>createProcessingEnvironment</tt>, you can manipulate the environment
        * before and after the processing:
        * <pre>
        * Environment env = myTemplate.createProcessingEnvironment(root, out);
        * env.include("include/common.ftl", null, true);  // before processing
        * env.process();
        * TemplateModel x = env.getVariable("x");  // after processing
        * </pre>
        *
        * @param rootMap the root node of the data model.  If null, an
        * empty data model is used. Can be any object that the effective object
        * wrapper can turn into a <tt>TemplateHashModel</tt> Basically, simple and
        * beans wrapper can turn <tt>java.util.Map</tt> objects into hashes
        * and the Jython wrapper can turn both a <tt>PyDictionary</tt> as well as any
        * object that implements <tt>__getitem__</tt> into a template hash.
        * Naturally, you can pass any object directly implementing
        * <tt>TemplateHashModel</tt> as well.
        * @param wrapper The object wrapper to use to wrap objects into
        * {@link TemplateModel} instances. If null, the default wrapper retrieved
        * by {@link Configurable#getObjectWrapper()} is used.
        * @param out the writer to output the text to.
        * @return the {@link freemarker.core.Environment Environment} object created for processing
        * @throws TemplateException if an exception occurs while setting up the Environment object.
        * @throws IOException if an exception occurs doing any auto-imports
        */

        public Core.Environment CreateProcessingEnvironment(object rootMap, TextWriter output, IObjectWrapper wrapper)
        {
            ITemplateHashModel root = null;
            if (rootMap is ITemplateHashModel)
            {
                root = (ITemplateHashModel) rootMap;
            }
            else
            {
                if (wrapper == null)
                {
                    wrapper = GetObjectWrapper();
                }

                try
                {
                    root = rootMap != null
                               ? (ITemplateHashModel) wrapper.Wrap(rootMap)
                               : new SimpleHash(wrapper);
                    if (root == null)
                    {
                        throw new ArgumentException(wrapper.GetType().FullName + " converted " +
                                                    rootMap.GetType().FullName + " to null.");
                    }
                }
                catch (InvalidCastException)
                {
                    throw new ArgumentException(wrapper.GetType().FullName + " could not convert " +
                                                rootMap.GetType().FullName + " to a TemplateHashModel.");
                }
            }
            return new Core.Environment(this, root, output);
        }

        /**
         * Same as <code>createProcessingEnvironment(rootMap, out, null)</code>.
         * @see #createProcessingEnvironment(object rootMap, Writer out, ObjectWrapper wrapper)
         */

        public Core.Environment CreateProcessingEnvironment(object rootMap, TextWriter output)
        {
            return CreateProcessingEnvironment(rootMap, output, null);
        }

        /**
         * Returns a string representing the raw template
         * text in canonical form.
         */

        public override string ToString()
        {
            var sw = new StringWriter();
            try
            {
                Dump(sw);
            }
            catch (IOException ioe)
            {
                throw new ApplicationException(ioe.Message);
            }
            return sw.ToString();
        }

        /**
         * The path of the template file relative to the directory what you use to store the templates.
         * For example, if the real path of template is <tt>"/www/templates/community/forum.fm"</tt>,
         * and you use "<tt>"/www/templates"</tt> as
         * {@link Configuration#setDirectoryForTemplateLoading "directoryForTemplateLoading"},
         * then <tt>name</tt> should be <tt>"community/forum.fm"</tt>. The <tt>name</tt> is used for example when you
         * use <tt>&lt;include ...></tt> and you give a path that is relative to the current
         * template, or in error messages when FreeMarker logs an error while it processes the template.
         */

        public string GetName()
        {
            return _name;
        }

        /**
         * Returns the Configuration object associated with this template.
         */

        public Configuration GetConfiguration()
        {
            return (Configuration) GetParent();
        }

        /**
         * Sets the character encoding to use for
         * included files. Usually you don't set this value manually,
         * instead it is assigned to the template upon loading.
         */

        public void SetEncoding(Encoding encoding)
        {
            _encoding = encoding;
        }

        /**
         * Returns the character encoding used for reading included files.
         */

        public Encoding GetEncoding()
        {
            return _encoding;
        }

        /**
         * Dump the raw template in canonical form.
         */

        public void Dump(TextWriter output)
        {
            output.Write(_rootElement.GetCanonicalForm());
        }

        /**
         * Called by code internally to maintain
         * a table of macros
         */
        public void AddMacro(Macro macro)
        {
            _macros.Add(macro.GetName(), macro);
        }

        /**
         * Called by code internally to maintain
         * a list of imports
         */

        public void AddImport(LibraryLoad ll)
        {
            _imports.Add(ll);
        }

        /**
         * Returns the template source at the location
         * specified by the coordinates given.
         * @param beginColumn the first column of the requested source, 1-based
         * @param beginLine the first line of the requested source, 1-based
         * @param endColumn the last column of the requested source, 1-based
         * @param endLine the last line of the requested source, 1-based
         * @see freemarker.core.TemplateObject#getSource()
         */

        public string GetSource(
            int beginColumn,
            int beginLine,
            int endColumn,
            int endLine)
        {
            // Our container is zero-based.
            --beginLine;
            --beginColumn;
            --endColumn;
            --endLine;
            var buf = new StringBuilder();
            for (int i = beginLine; i <= endLine; i++)
            {
                if (i < _lines.Count)
                {
                    buf.Append(_lines[i]);
                }
            }
            int lastLineLength = _lines[endLine].Length;
            int trailingCharsToDelete = lastLineLength - endColumn - 1;
            buf.Remove(0, beginColumn);
            buf.Remove(buf.Length - trailingCharsToDelete, trailingCharsToDelete);
            return buf.ToString();
        }

            /**
             * This is a helper class that builds up the line table
             * info for us.
             */
            private class LineTableBuilder : TextReader
            {
                private readonly Template _template;
                private readonly TextReader _reader;
                private readonly StringBuilder _lineBuf = new StringBuilder();
                private int _lastChar;

                /**
                 * @param r the character stream to wrap
                 */

                internal LineTableBuilder(Template template, TextReader r)
                {
                    _template = template;
                    _reader = r;
                }

                public override int Read()
                {
                    int c = _reader.Read();
                    HandleChar(c);
                    return c;
                }

                public override int Read(char[] cbuf, int off, int len)
                {
                    int numchars = _reader.Read(cbuf, off, len);
                    for (int i=off; i < off+numchars; i++) {
                        char c = cbuf[i];
                        HandleChar(c);
                    }
                    return numchars;
                }

                public override void Close()
                {
                    if (_lineBuf.Length > 0) {
                        _template._lines.Add(_lineBuf.ToString());
                        _lineBuf.Length = 0;
                    }
                    _reader.Close();
                }

                private void HandleChar(int c) {
                    if (c == '\n' || c == '\r') {
                        if (_lastChar == '\r' && c == '\n') { // CRLF under Windoze
                            int lastIndex = _template._lines.Count - 1;
                            string lastLine = _template._lines[lastIndex];
                            _template._lines[lastIndex] = lastLine + '\n';
                        } else {
                            _lineBuf.Append((char) c);
                            _template._lines.Add(_lineBuf.ToString());
                            _lineBuf.Length = 0;
                        }
                    }
                    else if (c == '\t') {
                        int numSpaces = 8 - (_lineBuf.Length % 8);
                        for (int i=0; i<numSpaces; i++) {
                            _lineBuf.Append(' ');
                        }
                    }
                    else {
                        _lineBuf.Append((char) c);
                    }
                    _lastChar = c;
                }
            }

        /**
         *  @return the root TemplateElement object.
         */

        public TemplateElement GetRootTreeNode()
        {
            return _rootElement;
        }

        public Dictionary<string, Macro> GetMacros()
        {
            return _macros;
        }

        public List<LibraryLoad> GetImports()
        {
            return _imports;
        }

        /**
         * This is used internally.
         */

        public void AddPrefixNsMapping(string prefix, string nsUri)
        {
            if (nsUri.Length == 0)
            {
                throw new ArgumentException("Cannot map empty string URI");
            }
            if (prefix.Length == 0)
            {
                throw new ArgumentException("Cannot map empty string prefix");
            }
            if (prefix == NoNsPrefix)
            {
                throw new ArgumentException("The prefix: " + prefix +
                                            " cannot be registered, it is reserved for special internal use.");
            }
            if (_prefixToNamespaceUriLookup.ContainsKey(prefix))
            {
                throw new ArgumentException("The prefix: '" + prefix + "' was repeated. This is illegal.");
            }
            if (_namespaceUriToPrefixLookup.ContainsKey(nsUri))
            {
                throw new ArgumentException("The namespace URI: " + nsUri + " cannot be mapped to 2 different prefixes.");
            }
            if (prefix == DefaultNamespacePrefix)
            {
                _defaultNs = nsUri;
            }
            else
            {
                _prefixToNamespaceUriLookup.Add(prefix, nsUri);
                _namespaceUriToPrefixLookup.Add(nsUri, prefix);
            }
        }

        public string GetDefaultNs()
        {
            return _defaultNs;
        }

        /**
         * @return the NamespaceUri mapped to this prefix in this template. (Or null if there is none.)
         */

        public string GetNamespaceForPrefix(string prefix)
        {
            if (string.IsNullOrEmpty(prefix))
            {
                return _defaultNs ?? string.Empty;
            }
            return _prefixToNamespaceUriLookup[prefix];
        }

        /**
         * @return the prefix mapped to this nsURI in this template. (Or null if there is none.)
         */

        public string GetPrefixForNamespace(string nsUri)
        {
            if (nsUri == null)
            {
                return null;
            }
            if (nsUri.Length == 0)
            {
                return _defaultNs == null ? "" : NoNsPrefix;
            }
            if (nsUri == _defaultNs)
            {
                return string.Empty;
            }
            return _namespaceUriToPrefixLookup[nsUri];
        }

        /**
         * @return the prefixed name, based on the ns_prefixes defined
         * in this template's header for the local name and node namespace
         * passed in as parameters.
         */

        public string GetPrefixedName(string localName, string nsUri)
        {
            if (string.IsNullOrEmpty(nsUri))
            {
                if (_defaultNs != null)
                {
                    return NoNsPrefix + ":" + localName;
                }
                else
                {
                    return localName;
                }
            }
            if (nsUri == _defaultNs)
            {
                return localName;
            }
            string prefix = GetPrefixForNamespace(nsUri);
            if (prefix == null)
            {
                return null;
            }
            return prefix + ":" + localName;
        }

        //    /**
        //     * @return an array of the elements containing the given column and line numbers.
        //     * @param column the column
        //     * @param line the line
        //     */
        //    public TreePath containingElements(int column, int line) {
        //        ArrayList elements = new ArrayList();
        //        TemplateElement element = rootElement;
        //mainloop:
        //        while (element.contains(column, line)) {
        //            elements.add(element);
        //            for (Enumeration enumeration = element.children(); enumeration.hasMoreElements();) {
        //                TemplateElement elem = (TemplateElement) enumeration.nextElement();
        //                if (elem.contains(column, line)) {
        //                    element = elem;
        //                    continue mainloop;
        //                }
        //            }
        //            break;
        //        }
        //        if (elements == null || elements.isEmpty()) {
        //            return null;
        //        }
        //        return new TreePath(elements.toArray());
        //    }

        public class WrongEncodingException : ParseException
        {
            public Encoding SpecifiedEncoding;

            public WrongEncodingException(Encoding specifiedEncoding)
            {
                SpecifiedEncoding = specifiedEncoding;
            }
        }
    }
}